/**
 * Period
 *
 * @author Chris Pratt
 *
 * 12/2/2010
 */
package com.anodyzed.onyx.util;

import com.anodyzed.onyx.text.TextFormat;

import java.io.Serializable;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;

@SuppressWarnings("serial")
public class Period implements Iterable<Date>, Serializable {
  private static final TextFormat toStr = new TextFormat("{0.start,date,yyyy-MM-dd HH:mm} - {0.end,date,yyyy-MM-dd HH:mm}");

  private Date start;
  private Date end;
  private Date inc = null;
  private int resolution = Calendar.MILLISECOND;

  /**
   * No-arg Bean Constructor
   */
  public Period () {
  } //Period

  /**
   * Constructor
   *
   * @param start The Beginning Date of the Period 
   * @param end The Final Date of the Period 
   */
  public Period (Date start,Date end) {
    this.start = start;
    this.end = end;
  } //Period

  /**
   * Constructor
   *
   * @param start The Starting Calendar 
   * @param end The Ending Calendar 
   */
  public Period (Calendar start,Calendar end) {
    this(start.getTime(),end.getTime());
  } //Period

  /**
   * Constructor
   *
   * @param start The Starting Date 
   * @param duration The duration of the Period 
   * @param unit The unit of the duration 
   */
  public Period (Date start,int duration,int unit) {
    this(Convert.toCalendar(start),duration,unit);
  } //Period

  /**
   * Constructor
   *
   * @param start The Starting Calendar 
   * @param duration The duration of the Period 
   * @param unit The unit of the duration 
   */
  public Period (Calendar start,int duration,int unit) {
    this.start = start.getTime();
    start.add(unit,duration);
    this.end = start.getTime();
  } //Period

  /**
   * Get the value of the start attribute. 
   *
   * @return The start value as a Date
   */
  public Date getStart () {
    return start;
  } //getStart

  /**
   * Set the value of the start attribute.
   *
   * @param start The new start value as a Date
   * @return this Period
   */
  public Period setStart (Date start) {
    this.start = start;
    return this;
  } //setStart

  /**
   * Set the value of the start attribute.
   *
   * @param cal The new start value as a Calendar
   * @return this Period
   */
  public Period setStart (Calendar cal) {
    return setStart(cal.getTime());
  } //setStart

  /**
   * Get the Duration of the Period.
   *
   * @return duration
   */
  public int getDuration () {
    return Chrono.change(end,start,resolution);
  } //getDuration

  /**
   * Set the value of the end attribute by specifying the duration of the period 
   *
   * @param duration The length of the Period
   * @param unit The duration unit (i.e. Calendar.DATE) 
   * @return this Period
   */
  public Period setDuration (int duration,int unit) {
    end = Chrono.add(start,duration - 1,unit);
    inc = null;
    resolution = unit;
    return this;
  } //setDuration

  /**
   * Calculate the duration (in milliseconds) between the two dates
   *
   * @return period duration (in ms) 
   */
  public long getDurationInMillis () {
    if((getStart() != null) && (getEnd() != null)) {
      return end.getTime() - start.getTime();
    }
    throw new NullPointerException("start and end dates cannot be null");
  } //getDurationInMillis

  /**
   * Get the duration between steps when using the iterator defined by this
   * Period.
   *
   * @return Iteration Resolution
   */
  public int getResolution () {
    return resolution;
  } //getResolution

  /**
   * Set the duration between steps when using the iterator defined by this
   * Period.
   *
   * @param resolution The Iteration Resolution
   * @return this Period
   */
  public Period setResolution (int resolution) {
    this.resolution = resolution;
    return this;
  } //setResolution

  /**
   * Get the value of the end attribute. 
   *
   * @return The end value as a Date
   */
  public Date getEnd () {
    return end;
  } //getEnd

  /**
   * Set the value of the end attribute.
   *
   * @param end The new end value as a Date
   * @return this Period
   */
  public Period setEnd (Date end) {
    this.end = end;
    return this;
  } //setEnd

  /**
   * Set the value of the end attribute.
   *
   * @param cal The new end value as a Calendar
   * @return this Period
   */
  public Period setEnd (Calendar cal) {
    return setEnd(cal.getTime());
  } //setEnd

  /**
   * Get the End Date that includes the last unit
   *
   * @return Inclusive End Date
   */
  private Date getEndInclusive () {
    if(inc == null) {
      inc = Chrono.add(end,1,resolution);
    }
    return inc;
  } //getEndInclusive

  /**
   * Create a Period from Midnight on the morning of the start date to midnight 
   * the night of the end date (i.e. midnight of the next morning). 
   *
   * @param start The Starting Date of the Unit
   * @param end The Ending Date of the Unit
   * @param unit The Calendar Unit
   * @return The Period
   */
  public static Period getPeriod (Date start,Date end,int unit) {
    return new Period().setStart(Chrono.clearTo(start,unit)).setEnd(Chrono.clearTo(end,unit)).setResolution(unit);
  } //getPeriod

  /**
   * Create a Period from Midnight on the morning of the start calendar to
   * midnight the night of the end calendar (i.e. midnight of the next morning).
   *
   * @param start The Starting Calendar of the Unit
   * @param end The Ending Calendar of the Unit
   * @param unit The Calendar Unit
   * @return The Period
   */
  public static Period getPeriod (Calendar start,Calendar end,int unit) {
    return new Period().setStart(Chrono.clearTo(start,unit)).setEnd(Chrono.clearTo(end,unit)).setResolution(unit);
  } //getPeriod

  /**
   * Create a Period from the Beginning <code>unit</code> of the
   * <code>start</code> time, going to the end of the time unit
   * <code>duration</code> units later.
   *
   * For example a Period from <code>2012/11/19 22:09:30</code> with a
   * <code>duration</code> of <code>7</code> and a <code>unit</code> of
   * <code>Calendar.DATE</code> would yield a Period stretching from
   * <code>2012/11/19 00:00:00</code> through <code>2012/11/27 00:00:00</code>
   *
   * @param start The Starting Date
   * @param duration The number of units in the Period
   * @param unit The Calendar Units for the duration
   * @return The Period
   */
  public static Period getPeriod (Date start,int duration,int unit) {
    return new Period().setStart(Chrono.clearTo(start,unit)).setEnd(Chrono.clearTo(Chrono.add(start,duration - 1,unit),unit)).setResolution(unit);
  } //getPeriod

  /**
   * Create a Period from the Beginning <code>unit</code> of the
   * <code>start</code> time, going to the end of the time unit
   * <code>duration</code> units later.
   *
   * For example a Period from <code>2012/11/19 22:09:30</code> with a
   * <code>duration</code> of <code>7</code> and a <code>unit</code> of
   * <code>Calendar.DATE</code> would yield a Period stretching from
   * <code>2012/11/19 00:00:00</code> through <code>2012/11/27 00:00:00</code>
   *
   * @param start The Starting Date
   * @param duration The number of units in the Period
   * @param unit The Calendar Units for the duration
   * @return The Period
   */
  public static Period getPeriod (Calendar start,int duration,int unit) {
    return new Period().setStart(Chrono.clearTo(start,unit)).setEnd(Chrono.clearTo(Chrono.add(start,duration - 1,unit),unit)).setResolution(unit);
  } //getPeriod

  /**
   * Test whether the supplied Date is contained within the Period.
   *
   * @param date The Date to check
   * @return true if <code>date</code> is within the Period
   */
  public boolean contains (Date date) {
    return !date.before(getStart()) && !date.after(getEndInclusive());
  } //contains

  /**
   * Test whether the supplied Calendar is contained within the Period.
   *
   * @param cal The Calendar to check
   * @return true if <code>cal</code> is within the Period
   */
  public boolean contains (Calendar cal) {
    return contains(cal.getTime());
  } //contains

  /**
   * Iterable: Iterate over the Period
   *
   * @return Date Iterator 
   */
  @Override
  public Iterator<Date> iterator () {
    return new Iterator<Date>() {
      Calendar cal = null;
      Calendar next = Convert.toCalendar(getStart());

      /**
       * Check if there's another unit in the Period
       *
       * @return true if more unit's available
       */
      @Override
      public boolean hasNext () {
        boolean ret = true;
        if(cal != null) {
          next.add(resolution,1);
          ret = !Chrono.after(next,getEnd(),resolution);
        }
        return ret;
      } //hasNext

      /**
       * Get the Date of the Next Unit
       *
       * @return Next unit's Date
       */
      @Override
      public Date next () {
        cal = next;
        return next.getTime();
      } //next

      /**
       * Remove the underlying Date from the Period?  This operation is not 
       * supported! 
       *
       * @exception UnsupportedOperationException Always! Because this Operation 
       *                                          is not supported
       */
      @Override
      public void remove () {
        throw new UnsupportedOperationException("It is not possible to remove a Date from a Period");
      } //remove
    };
  } //iterator

  /**
   * Return a String representation of the Period.
   *
   * @return String Period description
   */
  @Override
  public String toString () {
    return toStr.format(this);
  } //toString

} //*Period

